﻿using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using ValidationEngine.Commons;
using ValidationEngine.Commons.Interfaces;
using ValidationEngine.Tools;

namespace ValidationEngine.Tools
{
    /// <summary>
    /// Basic IValidator implementation
    /// </summary>
    public class BaseValidator<TModel> : IValidator where TModel: ValidateableModel
    {
        #region Members
        private Dictionary<string, List<IValidationRule>> _exactRules = new Dictionary<string, List<IValidationRule>>();
        private Dictionary<string, List<IValidationRule>> _constantRules = new Dictionary<string, List<IValidationRule>>();
        private Dictionary<string, List<ConditionalRuleContext>> _conditionalRules = new Dictionary<string, List<ConditionalRuleContext>>(); 
        #endregion

        #region IValidator
        public IValidationInfo Validate(IValidationContext ctx)
        {
            return DoValidate(ctx);
        }

        public Dictionary<string, IValidationInfo> ValidateAll(IValidationContext ctx)
        {
            Dictionary<string, IValidationInfo> res = new Dictionary<string, IValidationInfo>();

            foreach (var propertyRules in _exactRules)
            {
                if (!res.ContainsKey(propertyRules.Key))
                {
                    res.Add(propertyRules.Key, new ValidationInfo());
                }
                foreach (var rule in propertyRules.Value)
                {
                    ctx.PropertyName = propertyRules.Key;
                    res[propertyRules.Key].Add(rule.Apply(ctx));
                }
            }

            return res;
        }

        public Dictionary<string, IValidationInfo> ValidateAll(IValidateableModel model)
        {
            return ValidateAll(new ValidationContext { Model = model, PropertyName = string.Empty });
        }

        public void AddConditionalRule(string masterPropName, string slavePropName, IValidationRule rule)
        {
            if (!_conditionalRules.ContainsKey(masterPropName))
            {
                _conditionalRules.Add(masterPropName, new List<ConditionalRuleContext>());
            }
            _conditionalRules[masterPropName].Add(new ConditionalRuleContext { Rule = rule, SlaveProperty = slavePropName });
        }

        public void AddRule(string propName, IValidationRule rule, bool constantRule = false)
        {
            if (constantRule)
            {
                AddRule(propName, rule, _constantRules);
            }
            else
            {
                AddRule(propName, rule, _exactRules);
            }
        }
        #endregion

        #region Appling rules
        private Dictionary<string, IValidationInfo> ApplyConditionalRules(IValidationContext ctx)
        {
            Dictionary<string, IValidationInfo> condPropWithErrors = new Dictionary<string, IValidationInfo>();
            if (_conditionalRules.ContainsKey(ctx.PropertyName))
            {
                foreach (var conditionalCtx in _conditionalRules[ctx.PropertyName])
                {
                    if (!condPropWithErrors.ContainsKey(conditionalCtx.SlaveProperty))
                    {
                        condPropWithErrors.Add(conditionalCtx.SlaveProperty, new ValidationInfo());
                    }
                    condPropWithErrors[conditionalCtx.SlaveProperty].Add(conditionalCtx.Rule.Apply(ctx));
                }
            }

            return condPropWithErrors;
        }

        private IEnumerable<IValidationInfo> ApplyPropertyRules(IValidationContext ctx)
        {
            List<IValidationInfo> res = new List<IValidationInfo>();
            if (_exactRules.ContainsKey(ctx.PropertyName))
            {
                foreach (IValidationRule rule in _exactRules[ctx.PropertyName])
                {
                    res.Add(rule.Apply(ctx));
                }
            }
            return res;
        }

        private IEnumerable<IValidationInfo> ApplyConstantRules(IValidationContext ctx)
        {
            IValidationContext temp = new ValidationContext { Model = ctx.Model, PropertyName = null};
            List<IValidationInfo> res = new List<IValidationInfo>();
            foreach (var propRules in _constantRules)
            {
                temp.PropertyName = propRules.Key;

                foreach (var rule in propRules.Value)
                {
                    res.Add(rule.Apply(ctx));
                }
            }
            return res;
        }
        
        #endregion

        #region Tools
        /// <summary>
        /// Adds rule to corresponding internal collection.
        /// </summary>
        /// <param name="propName">Property name</param>
        /// <param name="rule">Validation rule</param>
        /// <param name="rules">Rules collection.</param>
        private void AddRule(string propName, IValidationRule rule, Dictionary<string, List<IValidationRule>> rules)
        {
            if (!rules.ContainsKey(propName))
            {
                rules.Add(propName, new List<IValidationRule>());
            }
            rules[propName].Add(rule);
        }

        /// <summary>
        /// Runs rules and sets validation info to model.
        /// </summary>
        /// <param name="ctx">Validation context</param>
        /// <returns>Validation info</returns>
        private IValidationInfo DoValidate(IValidationContext ctx)
        {
            var vi = new ValidationInfo();

            vi.AddRange(ApplyPropertyRules(ctx));
            vi.AddRange(ApplyConstantRules(ctx));
            ModelHelper.SetModelPropertyInfo(ctx, vi);
            vi.AddConditionals(ApplyConditionalRules(ctx));

            return vi;
        }

        private class ConditionalRuleContext
        {
            public string SlaveProperty { get; set; }
            public IValidationRule Rule { get; set; }
        } 
        #endregion

        #region Ctor
        protected BaseValidator()
        { }
        #endregion

        
        /// <summary>
        /// Returns property name from it's lambda representation
        /// </summary>
        /// <typeparam name="TProperty">Property</typeparam>
        /// <param name="expression">Property expression</param>
        /// <returns>Property name string equivalent</returns>
        /// <exception cref="ArgumentException">The given expression is not a MemberExpression.</exception>
        protected string Property<TProperty>(Expression<Func<TModel, TProperty>> expression)
        {
            var memberExpression = expression.Body as MemberExpression;

            if (memberExpression == null)
            {
                throw new ArgumentException("The given expression is not a MemberExpression.", "expression");
            }
            var propertyInfo = memberExpression.Member as PropertyInfo;
            if (propertyInfo == null)
            {
                throw new ArgumentException("The given expression is not a property.", "expression");
            }
            return propertyInfo.Name;
        }

        /// <summary>
        /// Casts validation context model to validator's model type
        /// </summary>
        /// <param name="ctx">Validation context</param>
        /// <returns>Validator's model from context</returns>
        protected TModel GetModel(IValidationContext ctx)
        {
            if (null == ctx || null == ctx.Model)
            {
                return null;
            }
            return ctx.Model as TModel;
        }
    }
}
